Skip to content

Support for transitive dependency removal#5549

Merged
mctofu merged 4 commits intomainfrom
mctofu/remove-dependency-via-flag
Aug 23, 2022
Merged

Support for transitive dependency removal#5549
mctofu merged 4 commits intomainfrom
mctofu/remove-dependency-via-flag

Conversation

@mctofu
Copy link
Copy Markdown
Contributor

@mctofu mctofu commented Aug 16, 2022

With security alerts on transitive dependencies we sometimes see that an update to the parent actually ends up removing the vulnerable dependency from the tree. That's a valid way to resolve an alert but removing dependencies is not a feature Dependabot currently supports. This adds limited support for dependency removal (transitive dependencies removed as part of parent upgrade).

The approach is:

  1. UpdateChecker will return removed transitive dependencies in the list of dependencies to update but they'll be flagged as removed
  2. FileUpdater will ignore dependencies flagged as removed since there's nothing to be done
  3. The PR description will indicate the dependency was removed
  4. When validating that the update fixes the vulnerability we'll consider removal of the dependency as fixed
  5. I've also removed the restriction on removing dependencies from the VulnerabilityAuditor but it's feature flagged

One complication is that we serialize the dependency information to other internal systems and perform validations in those places as well. Before this can be merged enabled I'll need to make corresponding updates to those systems to handle a removed dependency. We'll also need to maintain this feature flag for awhile to prevent errors if this code is used on an older version of GHES which wouldn't have the necessary support for removed dependencies.

I'd also considering using a blank target version as a signal of a removed dependency in #5487. However, after I discovered we also use this in other systems I felt a more explicit signal would be easier to understand.

dry-run and PR example
[dependabot-core-dev] ~/dependabot-core $ SECURITY_ADVISORIES='[{"dependency-name":"@dependabot-fixtures/npm-transitive-dependency","affected-versions":["<1.0.1"]}]' bin/dry-run.rb npm_and_yarn dependabot-fixtures/npm-locked-transitive-dependency-removed --security-updates-only --dep @dependabot-fixtures/npm-transitive-dependency --cache files --updater-options=npm_transitive_security_updates,npm_transitive_dependency_removal --pull-request
=> reading dependency files from cache manifest: ./dry-run/dependabot-fixtures/npm-locked-transitive-dependency-removed/cache-manifest-npm_and_yarn.json
=> parsing dependency files
=> updating 1 dependencies: @dependabot-fixtures/npm-transitive-dependency

=== @dependabot-fixtures/npm-transitive-dependency (1.0.0) (vulnerable 🚨)
 => checking for updates 1/1
 => latest available version is 1.0.1
 => earliest available non-vulnerable version is 1.0.1
 => latest allowed version is 1.0.0
 => requirements to unlock: all
 => requirements update strategy: bump_versions
 => updating @dependabot-fixtures/npm-remove-dependency from 10.0.0 to 10.0.1

    ± package.json
    ~~~
    13c13
    <     "@dependabot-fixtures/npm-remove-dependency": "10.0.0"
    ---
    >     "@dependabot-fixtures/npm-remove-dependency": "10.0.1"
    ~~~

    ± package-lock.json
    ~~~
    12c12
    <         "@dependabot-fixtures/npm-remove-dependency": "10.0.0"
    ---
    >         "@dependabot-fixtures/npm-remove-dependency": "10.0.1"
    16,26c16,18
    <       "version": "10.0.0",
    <       "resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-remove-dependency/-/npm-remove-dependency-10.0.0.tgz",
    <       "integrity": "sha512-QMgb6isjtNCnql6Nn+/h2v759qIW4f1ZDER8IbUJaIuyppDq3BqABbiwTFNenynu39yQZ1YAUFbWuNGeECNLyw==",
    <       "dependencies": {
    <         "@dependabot-fixtures/npm-transitive-dependency": "1.0.0"
    <       }
    <     },
    <     "node_modules/@dependabot-fixtures/npm-transitive-dependency": {
    <       "version": "1.0.0",
    <       "resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-transitive-dependency/-/npm-transitive-dependency-1.0.0.tgz",
    <       "integrity": "sha512-nFbzQH0TRgdzSA2/FH6MPnxZDpD+5Bgz00aD5Edgbc1wY/k8VC9s7lnk22dBTgJLwoY7MgbrnAf9rAvN08hHVg=="
    ---
    >       "version": "10.0.1",
    >       "resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-remove-dependency/-/npm-remove-dependency-10.0.1.tgz",
    >       "integrity": "sha512-acz1nPaB5TR8A5FBmOyZcYE8YQVs5TwrSGtqCyT0hbKJPtDYBt8yxRdbgC36fPrP/4JZn++bvaohG7IQq6NrPA=="
    31,41c23,25
    <       "version": "10.0.0",
    <       "resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-remove-dependency/-/npm-remove-dependency-10.0.0.tgz",
    <       "integrity": "sha512-QMgb6isjtNCnql6Nn+/h2v759qIW4f1ZDER8IbUJaIuyppDq3BqABbiwTFNenynu39yQZ1YAUFbWuNGeECNLyw==",
    <       "requires": {
    <         "@dependabot-fixtures/npm-transitive-dependency": "1.0.0"
    <       }
    <     },
    <     "@dependabot-fixtures/npm-transitive-dependency": {
    <       "version": "1.0.0",
    <       "resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-transitive-dependency/-/npm-transitive-dependency-1.0.0.tgz",
    <       "integrity": "sha512-nFbzQH0TRgdzSA2/FH6MPnxZDpD+5Bgz00aD5Edgbc1wY/k8VC9s7lnk22dBTgJLwoY7MgbrnAf9rAvN08hHVg=="
    ---
    >       "version": "10.0.1",
    >       "resolved": "https://registry.npmjs.org/@dependabot-fixtures/npm-remove-dependency/-/npm-remove-dependency-10.0.1.tgz",
    >       "integrity": "sha512-acz1nPaB5TR8A5FBmOyZcYE8YQVs5TwrSGtqCyT0hbKJPtDYBt8yxRdbgC36fPrP/4JZn++bvaohG7IQq6NrPA=="
    ~~~
Pull Request Title: Bump @dependabot-fixtures/npm-transitive-dependency and @dependabot-fixtures/npm-remove-dependency
--description--
Bumps [@dependabot-fixtures/npm-transitive-dependency](https://github.com/dependabot-fixtures/npm-transitive-dependency) and [@dependabot-fixtures/npm-remove-dependency](https://github.com/dependabot-fixtures/npm-remove-dependency). These dependencies needed to be updated together.
Removes `@dependabot-fixtures/npm-transitive-dependency`
Updates `@dependabot-fixtures/npm-remove-dependency` from 10.0.0 to 10.0.1
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/dependabot-fixtures/npm-remove-dependency/commit/84218f3c75c252017a921e0cbd9c7ec8cb91df51"><code>84218f3</code></a> Remove dependencies</li>
<li>See full diff in <a href="https://github.com/dependabot-fixtures/npm-remove-dependency/compare/v10.0.0...v10.0.1">compare view</a></li>
</ul>
</details>
<br />

--/description--
--commit--
Bump @dependabot-fixtures/npm-transitive-dependency and @dependabot-fixtures/npm-remove-dependency

Bumps [@dependabot-fixtures/npm-transitive-dependency](https://github.com/dependabot-fixtures/npm-transitive-dependency) and [@dependabot-fixtures/npm-remove-dependency](https://github.com/dependabot-fixtures/npm-remove-dependency). These dependencies needed to be updated together.

Removes `@dependabot-fixtures/npm-transitive-dependency`

Updates `@dependabot-fixtures/npm-remove-dependency` from 10.0.0 to 10.0.1
- [Release notes](https://github.com/dependabot-fixtures/npm-remove-dependency/releases)
- [Commits](https://github.com/dependabot-fixtures/npm-remove-dependency/compare/v10.0.0...v10.0.1)
--/commit--

@mctofu mctofu force-pushed the mctofu/remove-dependency-via-flag branch from f26122f to d2a6ee9 Compare August 17, 2022 04:35
@mctofu mctofu force-pushed the mctofu/remove-dependency-via-flag branch from d2a6ee9 to 02a52be Compare August 17, 2022 20:45
@mctofu mctofu marked this pull request as ready for review August 17, 2022 21:18
@mctofu mctofu requested a review from a team as a code owner August 17, 2022 21:18
Copy link
Copy Markdown
Contributor

@landongrindheim landongrindheim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some questions around how we're using booleans, but I see this doing what we want 😄

end

def removed?
@removed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 What do you think about this explicitly returning a boolean? I'd expect that based on the ? ending of the method name.

Suggested change
@removed
!@removed.nil?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should already return a boolean since I've defaulted it to false at https://github.com/dependabot/dependabot-core/pull/5549/files#diff-75ed2e4bde042ddd4c61aeb79efee8d861ecae1351d692fb94607b581d647f05R44

I just wanted the ? on the accessor. Would you do that another way?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd reach for the ? method as well 😄 I'd personally make sure to return true/false because it's a pretty established convention. The other option is !!@removed, but I think Rubocop favors this approach.

"package_manager" => package_manager,
"subdependency_metadata" => subdependency_metadata
"subdependency_metadata" => subdependency_metadata,
"removed" => removed? ? true : nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along the same lines, if we were to have #removed? return a boolean, we could just use it here and "removed" would always evaluate to true/false, which feels right to me. If there's an angle I'm not considering here, please let me know 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When false I'm setting it to nil here so that the call to #compact will remove it from the result. That's to avoid altering any behavior until we've made needed adjustments in other systems and turn the feature on.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 My next thought would be to make sure we're handling any use of #fetch to make sure we provide a fallback value in case it's been #compacted away. Glanced through the diff here and I think we're handling the one case that exists 🙂

return false unless affects_version?(dependency.previous_version)

# Removing a dependency is a way to fix the vulnerability
return true if dependency.removed?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 This reads so well!

"Bumps [statesman](https://github.com/gocardless/statesman) "\
"and [business](https://github.com/gocardless/business). "\
"These dependencies needed to be updated together.\n"\
"Removes `statesman`\n"\
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

Comment on lines +196 to +197
removed = update_details.fetch(:removed, false)
version = update_details.fetch(:version).to_s unless removed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 When I read this, I read that version is nil when dependency.removed? is true. Is that always the case? Could the absence of a version suggest removal?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this stage of the code, yes. However, it is valid for a Dependency to have just a requirement range (^6.0.0) and no specific version when we are parsing the dependency files and in those cases I didn't want them to be flagged as removed.

class UpdateChecker < Dependabot::UpdateCheckers::Base
class VulnerabilityAuditor
def initialize(dependency_files:, credentials:)
def initialize(dependency_files:, credentials:, allow_removal: false)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will allow_removal be toggled?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mctofu
Copy link
Copy Markdown
Contributor Author

mctofu commented Aug 23, 2022

Before this can be merged enabled I'll need to make corresponding updates to those systems to handle a removed dependency.

On further review, it's going to be easier to merge this first so I can make use of the updated code elsewhere. This change won't cause any downstream issues as long as the feature flag is not enabled.

@mctofu mctofu requested review from a team and landongrindheim August 23, 2022 17:06
@mctofu mctofu enabled auto-merge August 23, 2022 21:23
@mctofu mctofu merged commit b089cd3 into main Aug 23, 2022
@mctofu mctofu deleted the mctofu/remove-dependency-via-flag branch August 23, 2022 21:29
@mctofu mctofu mentioned this pull request Aug 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants